//go:build ignore

package main

import (
	"bytes"
	"cmp"
	"encoding/json"
	"flag"
	"fmt"
	"go/format"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"unicode"

	"github.com/microsoft/typescript-go/internal/repo"
)

type diagnosticMessage struct {
	Category           string `json:"category"`
	Code               int    `json:"code"`
	ReportsUnnecessary bool   `json:"reportsUnnecessary"`
	ReportsDeprecated  bool   `json:"reportsDeprecated"`
	// spelling error here is [sic] in Strada
	ElidedInCompatibilityPyramid bool `json:"elidedInCompatabilityPyramid"`

	key string
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	input := filepath.Join(repo.TypeScriptSubmodulePath, "src", "compiler", "diagnosticMessages.json")
	if _, err := os.Stat(input); err != nil {
		log.Fatalf("failed to find input file: %v", err)
		return
	}

	output := flag.String("output", "", "path to the output diagnostics_generated.go file")
	flag.Parse()

	if *output == "" {
		flag.Usage()
		return
	}

	inputFile, err := os.Open(input)
	if err != nil {
		log.Fatalf("failed to open input file: %v", err)
		return
	}
	defer inputFile.Close()

	var rawDiagnosticMessages map[string]*diagnosticMessage
	if err := json.NewDecoder(inputFile).Decode(&rawDiagnosticMessages); err != nil {
		log.Fatalf("failed to decode input file: %v", err)
		return
	}

	diagnosticMessages := make([]*diagnosticMessage, 0, len(rawDiagnosticMessages))
	for k, v := range rawDiagnosticMessages {
		v.key = k
		diagnosticMessages = append(diagnosticMessages, v)
	}

	slices.SortFunc(diagnosticMessages, func(a *diagnosticMessage, b *diagnosticMessage) int {
		return cmp.Compare(a.Code, b.Code)
	})

	var buf bytes.Buffer

	buf.WriteString("// Code generated by generate.go; DO NOT EDIT.\n")
	buf.WriteString("\n")
	buf.WriteString("package diagnostics\n")

	for _, m := range diagnosticMessages {
		varName, key := convertPropertyName(m.key, m.Code)

		fmt.Fprintf(&buf, "var %s = &Message{code: %d, category: Category%s, key: %q, text: %q", varName, m.Code, m.Category, key, m.key)

		if m.ReportsUnnecessary {
			buf.WriteString(`, reportsUnnecessary: true`)
		}
		if m.ElidedInCompatibilityPyramid {
			buf.WriteString(`, elidedInCompatibilityPyramid: true`)
		}
		if m.ReportsDeprecated {
			buf.WriteString(`, reportsDeprecated: true`)
		}

		buf.WriteString("}\n")
	}

	formatted, err := format.Source(buf.Bytes())
	if err != nil {
		log.Fatalf("failed to format output: %v", err)
		return
	}

	if err := os.WriteFile(*output, formatted, 0o666); err != nil {
		log.Fatalf("failed to write output: %v", err)
		return
	}
}

var (
	multipleUnderscoreRegexp           = regexp.MustCompile(`_+`)
	leadingUnderscoreUnlessDigitRegexp = regexp.MustCompile(`^_+(\D)`)
	trailingUnderscoreRegexp           = regexp.MustCompile(`_$`)
)

func convertPropertyName(origName string, code int) (varName string, key string) {
	var b strings.Builder
	b.Grow(len(origName))

	for _, r := range origName {
		switch r {
		case '*':
			b.WriteString("_Asterisk")
		case '/':
			b.WriteString("_Slash")
		case ':':
			b.WriteString("_Colon")
		default:
			if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
				b.WriteRune('_')
			} else {
				b.WriteRune(r)
			}
		}
	}

	varName = b.String()
	// get rid of all multi-underscores
	varName = multipleUnderscoreRegexp.ReplaceAllString(varName, "_")
	// remove any leading underscore, unless it is followed by a number.
	varName = leadingUnderscoreUnlessDigitRegexp.ReplaceAllString(varName, "$1")
	// get rid of all trailing underscores.
	varName = trailingUnderscoreRegexp.ReplaceAllString(varName, "")

	key = varName
	if len(key) > 100 {
		key = key[:100]
	}
	key = key + "_" + strconv.Itoa(code)

	if !token.IsExported(varName) {
		var b strings.Builder
		b.Grow(len(varName) + 2)
		if varName[0] == '_' {
			b.WriteString("X")
		} else {
			b.WriteString("X_")
		}
		b.WriteString(varName)
		varName = b.String()
	}

	if !token.IsIdentifier(varName) || !token.IsExported(varName) {
		log.Fatalf("failed to convert property name to exported identifier: %q", origName)
	}

	return varName, key
}
